<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>基于Swoole的WebSocket聊天室</title>
    <link rel="stylesheet" href="./static/bootstrap.min.css">
    <link rel="stylesheet" href="./static/bootstrap-theme.min.css">
    <style>
        .typing::after {
            content: '▌';
        }
        .blinker::after {
           animation: blinker 1s step-end infinite;
        }
        @keyframes blinker {
            0% {
                visibility: visible;
            }
            50% {
                visibility: hidden;
            }
            100% {
                visibility: visible;
            }
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="page-header">
            <h2>波波聊天室</h2>
        </div>
        <div class="col-xs-8 col-sm-8 col-md-8">
            <div class="form-group">
                <div class="col-xs-12 col-sm-12 col-md-12" style="height: 550px; border: 1px solid #e4b9b9;overflow-y: scroll" id="chatFrame"></div>

                <textarea name="message" rows="4" class="form-control col-xs-12 col-sm-12 col-md-12" placeholder="请输入聊天内容"></textarea>
                <select class="input-sm" id="target">
                    <option value="0">全体</option>
                </select>
                <button type="button" class="btn btn-success btn-block send-button">发送</button>
            </div>
            <div class="form-group send-message">

            </div>
        </div>
        <div class="col-xs-4 col-sm-4 col-md-4">
            <div class="col-xs-12 col-sm-12 col-md-12">
                <h3>当前用户</h3>
                <table class="table table-hover mine"></table>
            </div>
            <div class="col-xs-12 col-sm-12 col-md-12">
                <h3>在线用户</h3>
                <table class="table table-hover online-user">
                    <tr>
                        <th>编号</th>
                        <th>线ID</th>
                        <th>用户名</th>
                    </tr>
                </table>
            </div>
            <div class="col-xs-12 col-sm-12 col-md-12">
                <h3>上下线通知</h3>
                <table class="table table-hover onOff"></table>
            </div>
        </div>
    </div>
</div>

<script src="./static/jquery.js"></script>
<script src="./static/bootstrap.min.js"></script>
<script src="./static/cookie.js"></script>
<script>
    var user_id,user_name;
    var lockReconnect = false;//避免重复连接
    var limitReconnect = 5;   //重连次数
    var ws = null;            //WebSocket的引用
    var chatFrameDoc = document.getElementById("chatFrame");
    var chatLogNumberCount = 0;

    setUserName();
    createWebSocket();

    //设置名字
    function setUserName() {
        user_name = getCookie("username");
        if (!user_name) {
            user_name = prompt("请输入你的昵称");
            if (!user_name) {
                user_name = "user" + Math.floor(Math.random()*100000 + 1)
            }
            setCookie("username", user_name, 1);
        }
        $('.mine').html(`<tr><td>${user_name} <a class="badge btn-danger alert-name">修改</a></td></tr>`);
    }

    //创建WebSocket
    function createWebSocket(){
        ws = new WebSocket('ws://127.0.0.1:9502?user_name='+user_name);

        // 连接成功建立后响应
        ws.onopen = function() {
            //心跳检测重置
            heartCheck.reset().start();
        }

        // 收到服务器消息后响应
        ws.onmessage = function(evt) {
            let data = [];
            if (evt.data) {
                data = jQuery.parseJSON(evt.data);
                console.log(data);

                switch (data.type) {
                    case "open":
                        data = data.data;
                        user_id = data.mine;
                        $.each(data, function (key,val) {
                            switch (key) {
                                case 'last':
                                    // if (val.length > 0) {
                                    //     let html = '';
                                    //     let is_myself = '';
                                    //     $.each(val, function (k, vo) {
                                    //         if (user_id == vo.user_id) {
                                    //             is_myself = ' text-right';
                                    //         } else {
                                    //             is_myself = '';
                                    //         }
                                    //         html += '<div class="panel panel-success">' +
                                    //             '<div class="panel-heading ' + is_myself + '">【' + vo.user_name + '】 ' + vo.create_at + '</div>' +
                                    //             '<div class="panel-body ' + is_myself + '">' + vo.content +
                                    //             '</div></div>';
                                    //     });
                                    //     $('.send-message').prepend(html);
                                    // }

                                    if (val.length > 0) {
                                        let html = '';
                                        let is_myself = '';
                                        $.each(val, function (k, vo) {
                                            if (user_id == vo.user_id) {
                                                is_myself = ' text-right';
                                            } else {
                                                is_myself = '';
                                            }
                                            html += `<div class="${is_myself}"><p class="text-danger">${vo.user_name} ${vo.create_at}</p><p>${vo.content}</p>`;
                                        });
                                        $('#chatFrame').append(html);
                                        chatFrameDoc.scrollTo(0, chatFrameDoc.scrollHeight)
                                    }
                                    break;
                                case 'online':
                                    if (val.length > 0) {
                                        let open_html = '', target_html = '';
                                        $.each(val, function (k, vo) {
                                            open_html += `<tr data-id="${vo.fd}"><td>${vo.id}</td><td>${vo.fd}</td><td>${vo.user_name}</td></tr>`;
                                            target_html += `<option value="${vo.fd}">${vo.user_name}</option>`
                                        });
                                        $('.online-user').append(open_html);
                                        $("#target option").after(target_html);
                                    }
                                    break;
                            }
                        })
                        break;
                    case "chat":
                        // 聊天消息推送
                        let is_myself = '', id = "c" + user_id + chatLogNumberCount;
                        chatLogNumberCount ++;

                        if (data.data.from == data.data.to) {
                            is_myself = ' text-right';
                        }
                        let html = `<div class="${is_myself}"><p class="text-danger">${data.data.user_name } ${data.data.time}</p><p id="${id}"></p>`;
                        $('#chatFrame').append(html);
                        printText(document.getElementById(id), data.msg);
                        chatFrameDoc.scrollTo(0, chatFrameDoc.scrollHeight);
                        break;
                    case "on":
                        // 新用户连接
                        let val = data.data;
                        //在线用户增加一个
                        $('.online-user').append(`<tr data-id="${val.fd}"><td>${val.id}</td><td>${val.fd}</td><td>${val.user_name}</td></tr>`);
                        // 上线欢迎
                        $('.onOff').prepend(`<div class="panel panel-info"><div class="panel-body text-center">${data.msg}</div></div>`);
                        //增加一个发送对象
                        $("#target option").last().after(`<option value="${val.fd}">${val.user_name}</option>`);
                        break;
                    case "off":
                        // 用户关闭连接
                        $('.online-user').find('tr[data-id=' + data.data.fd + ']').remove();
                        // 下线提醒
                        $('.onOff').prepend(`<div class="panel panel-danger"><div class="panel-body text-center">${data.msg}</div></div>`);
                        //减少一个发送对象
                        let obj = $("#target");
                        let opt = obj.children("option");
                        let len = opt.length;
                        for (let i=0; i<len; i++) {
                            if (opt.eq(i).val() == data.data.fd) {
                                opt.eq(i).remove();
                            }
                        }
                        break;
                    case "pong":
                        //忽略心跳的信息
                        break;
                }
                //忽略心跳的信息，因为只要有消息进来，断线重连就会重置不会触发
                lockReconnect = false;
                limitReconnect = 5;
                heartCheck.reset().start();
            }
        }

        // 连接关闭后响应
        ws.onclose = function() {
            console.log("==========>onclose");
            reconnect();//重连
        }

        ws.onerror = function () {
            console.log("==========>onerror");
            reconnect();//重连
        };
    }

    // 断线重连
    function reconnect() {
        if (!lockReconnect && limitReconnect > 0) {
            console.log("==========>reconnect");
            lockReconnect = true; //防止onclose、onerror发送两次重连
            setTimeout(function () {
                createWebSocket();
                lockReconnect = false;
                limitReconnect --;
                console.log("正在重连，当前时间"+new Date())
            }, 5000);
        }
    }

    //心跳检测
    var heartCheck = {
        timeout: 50000,//毫秒
        timeoutObj: null,
        serverTimeoutObj: null,
        reset: function(){
            clearTimeout(this.timeoutObj);
            clearTimeout(this.serverTimeoutObj);
            return this;
        },
        start: function(){
            var self = this;
            this.timeoutObj = setTimeout(function(){
                //这里发送一个心跳，后端收到后，返回一个心跳消息，
                //onmessage拿到返回的心跳就说明连接正常
                ws.send(JSON.stringify({type:"ping"}));
                console.log("HeartBeat");
                self.serverTimeoutObj = setTimeout(function(){
                    //如果超过一定时间还没重置，说明后端主动断开了
                    ws.close();//如果onclose会执行reconnect，我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
                }, self.timeout)
            }, this.timeout)
        }
    }

    //发送内容
    $(".send-button").on('click', function () {
        let message = $("textarea[name=message]").val().trim();
        if (message == '') {
            //alert('请输入聊天内容……');
            return false;
        }
        let dt = JSON.stringify({
            fd: $("#target").children("option:selected").val(),
            message: message,
        });      
        ws.send(dt);
        $("textarea[name=message]").val('');
    });

    //修改昵称
    $(".alert-name").on('click', function (){
        let user_name = prompt("请输入你的昵称");
        if (user_name) {
            setCookie("username", user_name, 1);
            window.location.reload();
        }
    })
</script>

<script>
/**
* @description:
* @param {HTMLElement} dom - 打印内容的dom
* @param {string} content - 打印文本内容
* @param {number} speed - 打印速度
* @return {void}
*/
function printText(dom, content, speed = 200) {
    let index = 0
    setCursorStatus(dom, 'typing')
    let printInterval = setInterval(() => {
        dom.innerText += content[index]
        index++
        if (index >= content.length) {
            setCursorStatus(dom, 'end')
            clearInterval(printInterval)
        }
    }, speed)
}

/**
 * @description: 设置dom的光标状态
 * @param {HTMLElement} dom - 打印内容的dom
 * @param {"loading"|"typing"|"end"} status - 打印状态
 * @return {void}
 */
function setCursorStatus(dom, status) {
    const classList = {
        loading: 'typing blinker',
        typing: 'typing',
        end: '',
    }
    dom.className = classList[status]
}
</script>
</body>
</html>